iT邦幫忙

2022 iThome 鐵人賽

DAY 10
0
自我挑戰組

設計模式探索系列 第 10

[Day 10] 裝飾器模式 (2)

  • 分享至 

  • xImage
  •  

裝飾器模式

在咖啡這個案例中,我們不希望由繼承這麼僵化的架構來實作,因此我們使用裝飾器模式。而在裝飾器模式中,我們先打造基底部分,其他配料都在"執行期"來進行裝飾;例如前面所舉例的DarkRoastWithSoy,就先打造一個DarkRoast物件,因為想要加入Soy,我們就再建立一個Soy物件,且它的型態與DarkRoast是一樣的,也就是這個經過裝飾的物件跟裝飾前的物件是一樣的,都可以視為是Beverage,也都可以呼叫它的Cost()getDescription()方法;以此類推,外面也可以再包一層Soy(雙倍豆奶?),或再來一個Cream,而不管在哪一層,都可以呼叫Cost()getDescription()這兩個方法。
而當在最外面呼叫Cost(),就會一路叫回去,直到DarkRoast,再一路將每個調味料的價格加回來,獲得最終的價格。
裝飾器的特性如下:

  • 裝飾器的型態與被它裝飾的物件一樣
  • 可以使用多個裝飾器來包裝一個物件
  • 可以使用裝飾好的物件來取代包裝前的物件來傳遞
  • 裝飾器在將工作委託給被裝飾的物件之前或之後可以加入自己的行為
  • 物件可以在執行期動態地裝飾物件

裝飾器模式定義 (Decorator Pattern)

裝飾器模式的定義如下

裝飾器模式可以動態為物件附加額外的職責;使用裝飾器來擴展功能比使用繼承更有彈性

我們可以看一下類別圖來更清楚地了解他們的關係:
https://ithelp.ithome.com.tw/upload/images/20220925/20140096o1m4KRue5Y.png

修改後的架構

在咖啡的案例,對應的圖該如何長呢?可以思考一下,再往下看:
https://ithelp.ithome.com.tw/upload/images/20220925/20140096r0N8sOJqMW.png

雖然圖中用的是 "繼承",但實際上這個繼承是用來獲得相同的型態,而不是去繼承行為─例如去覆寫掉加了豆漿的某咖啡就該如何如何等等,可以稍微比較一下跟前一天爆炸的繼承圖的關係。
而實際程式又如何設計呢?首先原始的Beverage物件不需要改變:

class Beverage
{
    public:
        string description;
        virtual string getDescription()=0;
        virtual double cost()=0;
};

另外則是各種調味料(裝飾器)的類別:

class Decorator: public Beverage
{
    public:
        Beverage *beverage;
};

最核心的幾種飲料則如下:

class DarkRoast: public Beverage
{
    public:
        DarkRoast()
        {
            description = "Dark Roast";
        }

        string getDescription() override
        {
            return description;
        }

        double cost() override
        {
            return 60;
        }
};

最後則是各種調味料(裝飾器):

class Soy: public Decorator
{
    public:
        Soy(Beverage *b)
        {
            beverage = b;
        }

        string getDescription() override
        {
            return beverage->getDescription() + ", soy";
        }

        double cost() override
        {
            return beverage->cost() + 15;
        }
};

class Cream: public Decorator
{
    public:
        Cream(Beverage *b)
        {
            beverage = b;
        }

        string getDescription() override
        {
            return beverage->getDescription() + ", cream";
        }

        double cost() override
        {
            return beverage->cost() + 5;
        }
};

現在就可以做出一個DarkRoastWithDoubleSoyandCream來看看!

int main()
{
    Beverage *basicCoffee = new DarkRoast();
    cout << basicCoffee->getDescription() << " " << "$" << basicCoffee->cost() << endl;
    
    Beverage *myCoffee = new DarkRoast();
    myCoffee = new Soy(myCoffee);
    myCoffee = new Soy(myCoffee);
    myCoffee = new Cream(myCoffee);
    cout << myCoffee->getDescription() << " " << "$" << myCoffee->cost() << endl;
    
    return 0;
}
// -----output-----
/*
Dark Roast $60
Dark Roast, soy, soy, cream $95
*/

在後面的其他模式中,我們可以看到要怎麼更有效地製作裝飾器,讓建立裝飾器的作法是不會被隨意破壞的。

實際應用中的裝飾器模式

前面一直以飲料來舉例,而書中提出了一個實際上常被使用的API案例:Java的I/O,例如各種對InputStream做裝飾的小類別,PushbackInputStreamZipInputStream等等;或者說也可以自己寫一個lowerCaseInputStream,吃進string之後傳出小寫的string,再來一個trimedInputStream,傳出去掉空格的string;可以看到就可以直接包來包去,彈性使用。

結語

看到後面其實感覺這個飲料的舉例會有些瑕疵,例如說如果要半糖或少糖這種控制就不是裝飾器模式可以直接達到的XD 但是將裝飾器 = 調味料讓人可以比較快理解裝飾器模式的架構概念;到後面看到舉例 java.io 就可以更直接地明白這種模式的威力,畢竟修飾一個 InputStream 字串就不會有半份一份的問題~ 因此看的時候還是可以多多思考在實際應用中會有哪些不足之處,然後再看看有沒有違反原則,或有無其他模式可以更好地解決這些問題~? 也可以去查一下有沒有其他實際運用的場景。
預告接下來的是 工廠模式,也是現實中超級常運用的概念~!


上一篇
[Day 9] 裝飾器模式 (1)
下一篇
[Day 11] 工廠模式 (1)
系列文
設計模式探索30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言